#!/usr/bin/env python

'''
	Copyright (C) 2010  Pier Maria Mattioli
	
	This program is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	
	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.
	
	You should have received a copy of the GNU General Public License
	along with this program.  If not, see <http://www.gnu.org/licenses/>.
	
'''

import os
from errno import ENOENT
from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR
from time import time
from fuse import FuseOSError, Operations, LoggingMixIn
from clusty import Cluster
from fuse import FUSE


class ReFuse(Operations):
	
	def __init__(self, driver):
		now = time()
		self.fd = 0
		self.chunksize = 1024 * 1024 # 1MB
		self.driver = driver
		set_list = [('rfs::/::st_mode', (S_IFDIR | 0755)),
					('rfs::/::st_ctime', now),
					('rfs::/::st_mtime', now),
					('rfs::/::st_atime', now),
					('rfs::/::st_size', 0)]
		self.driver.mset(set_list)
		
	def chmod(self, path, mode):
		old_mode = self.driver.get('rfs::%s::st_mode' % path)
		new_mode = old_mode & 0770000 | mode
		self.driver.set('rfs::%s::st_mode' % path, new_mode)
		return 0

	def chown(self, path, uid, gid):
		self.driver.mset([('rfs::%s::st_uid' % path, uid),
						   ('rfs::%s::st_gid' % path, gid)])
	
	def create(self, path, mode):
		now = time()
		self.driver.mset([
			('rfs::%s::st_mode' % path, (S_IFREG | mode)),
			('rfs::%s::st_ctime' % path, now),
			('rfs::%s::st_mtime' % path, now),
			('rfs::%s::st_atime' % path, now),
			('rfs::%s::st_size' % path, 0),
			('rfs::%s::content' % path, '')])
		self.fd += 1
		return self.fd
	
	def getattr(self, path, fh=None):
		keys = ['rfs::%s::st_mode' % path,
				'rfs::%s::st_size' % path,
				'rfs::%s::st_ctime' % path,
				'rfs::%s::st_atime' % path,
				'rfs::%s::st_mtime' % path ]
		attr = self.driver.mget(keys)
		if None in attr:
			for k in keys:
				self.driver.delete(k)
			raise FuseOSError(ENOENT)
		out = {}
		for pos, k in enumerate(keys):
			nk = k.replace('rfs::%s::' % path, '', 1)
			out[nk] = attr[pos]
			
		if S_ISDIR(out['st_mode']):
			if path.endswith('/'): path = path[:-1]
			links = self.driver.keys('rfs::%s/*::st_mode' % path)
			nlink = len(links or '')
			out['st_nlink'] = nlink + 2
		else:
			out['st_nlink'] = 1
		return out
	
	def getxattr(self, path, name, position=0):
		return self.driver.get('rfs::%s::_x_%s' % (path, name), '')
		
	def listxattr(self, path):
		out = []
		for key in self.driver.keys('rfs::%s::_x_*' % path):
			attrname = key.replace('rfs::%s::_x_' % path, '', 1)
			out.append(attrname)
		return out
		
	def mkdir(self, path, mode):
		if path.endswith('/'): path = path[:-1]
		now = time()
		self.driver.mset([
			('rfs::%s::st_mode' % path, (S_IFDIR | mode)),
			('rfs::%s::st_ctime' % path, now),
			('rfs::%s::st_mtime' % path, now),
			('rfs::%s::st_atime' % path, now),
			('rfs::%s::st_size' % path, 0) ])

	def open(self, path, flags):
		self.fd += 1
		return self.fd
	
	def read(self, path, size, offset, fh):
		sb = int(offset / self.chunksize)
		eb = int((offset + size) / self.chunksize)
		chunk_offset = offset - (sb * self.chunksize)
		keys = ['rfs::%s::content-%d' % (path, i) for i in range(sb, eb + 1)]
		get_list = [(k, '') for k in keys]
		content = self.driver.mget(get_list)
		content = ''.join(content)
		return content[offset:(offset+size)]
	
	def readdir(self, path, fh):
		if path.endswith('/'): path = path[:-1]
		keys = self.driver.keys('rfs::%s/*::st_ctime' % path)
		keys.sort()
		dircontent = ['.', '..']
		for key in keys:
			file_name = key.replace('rfs::%s/' % path, '', 1)[:-10]
			file_name = file_name.split('/', 1)[0]
			if file_name and file_name not in dircontent:
				dircontent.append(file_name)
		return dircontent
	
	def readlink(self, path):
		return self.driver.get('rfs::%s::content' % path)
		
	def removexattr(self, path, name):
		self.driver.delete('rfs::%s::_x_%s' % (path, name))
		
	def rename(self, old, new):
		st_mode = self.driver.get('rfs::%s::st_mode' % old)
		if S_ISDIR(st_mode):
			if old.endswith('/'): old = old[:-1]
			pattern = 'rfs::%s[/:]*' % old
			
		else:
			pattern = 'rfs::%s::*' % old
		get_list = [k for k in self.driver.keys(pattern)]
		trasf = self.driver.mget(get_list)
		set_list = []
		for pos, k in get_list:
			set_list.append((k.replace(old, new, 1), trasf[pos]))
		self.driver.mset(set_list)
		for k in trasf.keys():
			self.driver.delete(k)
		
	def rmdir(self, path):
		if path.endswith('/'): path = path[:-1]
		for k in self.driver.keys('rfs::%s::*' % path):
			self.driver.delete(k)
	
	def setxattr(self, path, name, value, options, position=0):
		self.driver.set('rfs::%s::_x_%s' % (path, name), value)
	
	def statfs(self, path):
		return {'f_blocks': 4096, 'f_bsize': 1048576, 'f_bavail': 2048}
	
	def symlink(self, target, source):
		now = time()
		self.driver.mset([
			('rfs::%s::st_mode' % target, (S_IFLNK | 0777)),
			('rfs::%s::st_size' % target, len(source)),
			('rfs::%s::content' % target, source),
			('rfs::%s::st_ctime' % target, now),
			('rfs::%s::st_mtime' % target, now),
			('rfs::%s::st_atime' % target, now)])
	
	def truncate(self, path, length, fh=None):
		bn = int(length / self.chunksize)
		chunk_offset = length - (bn * self.chunksize)	
		chunk_key = 'rfs::%s::content-%d' % (path, bn)
		result = self.driver.mget(['rfs::%s::st_size' % path, chunk_key ])
		
		size = result[0]
		if result[1]:
			new_cont = result[1][:chunk_offset]
		else:
			new_cont = ''
		
		delchunk = ['rfs::%s::content-%d' % (path, i)
					for i in range(bn, (int(size / self.chunksize) + 1))]
		self.driver.mset(delchunk)
		
		setchunk = [('rfs::%s::st_size' % path, length)]
		if new_cont:
			setchunk.append((chunk_key, new_cont))
		self.driver.mset(setchunk)
	
	def unlink(self, path):
		for k in self.driver.keys('rfs::%s::*' % path):
			self.driver.delete(k)		
	
	def utimens(self, path, times=None):
		now = time()
		atime, mtime = times if times else (now, now)
		self.driver.mset([
			('rfs::%s::st_atime' % path, atime),
			('rfs::%s::st_mtime' % path, mtime) ])
	
	def write(self, path, data, offset, fh):
		data_len = len(data)
		sb = int(offset / self.chunksize)
		eb = int((offset + data_len) / self.chunksize)
		chunk_offset = offset - (sb * self.chunksize)
		
		keys = ['rfs::%s::content-%d' % (path, i) for i in range(sb, eb + 1)]
		get_list = [(k, '') for k in keys]
		get_list.append('rfs::%s::st_size' % path)
		content = self.driver.mget(get_list)
		size = content[-1]
		content = ''.join(content[:-1])

		newcontent = content[:chunk_offset] + data 
		if len(content) > (chunk_offset + data_len):
			newcontent += content[(chunk_offset + data_len):]
		else:
			size += (chunk_offset + data_len) - len(content)
			
		set_list = []
		for i,k in enumerate(keys):
			v = newcontent[(self.chunksize * i):(self.chunksize * (i+1))]
			set_list.append((k,v))
		set_list.append(('rfs::%s::st_size' % path, size))			
		self.driver.mset(set_list)
		return len(data)


def daemonize():
	import os
	import sys
	import resource

	UMASK = 0
	WORKDIR = "/"
	MAXFD = 1024
	
	if (hasattr(os, "devnull")):
		REDIRECT_TO = os.devnull
	else:
		REDIRECT_TO = "/dev/null"

	try:
		pid = os.fork()
	except OSError, e:
		raise Exception, "%s [%d]" % (e.strerror, e.errno)
	if (pid == 0):
		os.setsid()
		try:
			pid = os.fork()
		except OSError, e:
			raise Exception, "%s [%d]" % (e.strerror, e.errno)
		if (pid == 0):
			os.chdir(WORKDIR)
			os.umask(UMASK)
		else:
			os._exit(0)
	else:
		os._exit(0)
	
	maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
	if (maxfd == resource.RLIM_INFINITY):
		maxfd = MAXFD
	for fd in range(0, maxfd):
		try:
			os.close(fd)
		except OSError:
			pass
	os.open(REDIRECT_TO, os.O_RDWR)
	os.dup2(0, 1)
	os.dup2(0, 2)
	return(0)
	   
if __name__ == '__main__':
	import sys
	if len(sys.argv) < 3:
		sys.exit('usage: python rfs.py <mount_point> <node_id> [<node_id>, ...]')
	mountpoint = sys.argv[1]
	nodes = sys.argv[2:]
	daemonize()
	driver = Cluster(nodes)
	fuse = FUSE(ReFuse(driver), mountpoint, foreground=True, nothreads=True)
	driver.shutdown()
